Repository navigation
fix: redraw only the views whose moving pictures are due, at most 30 times a second - #49
Conversation
|
@vbmithr thanks for the profile in #39. It made the cause easy to find. Here's what this PR does with it, and how to check it on your machine. What changed (
What to expect: without caching, notifying one view still redraws the whole window, because the root view gets marked dirty and nothing is cached. For now the saving comes from the floor: by the arithmetic, about 2x fewer full redraws for 16 ms GIFs and about 1.6x for 20 ms ones. I haven't profiled it, so your uprobe numbers before and after would be the real measurement. They'd also tell us whether the caching pass is worth doing. To try it: git fetch origin pull/49/head:pr-49 && git checkout pr-49
cargo test -p tinyclient --features gui gui::anim # the two new tests
cargo run -p tinyclient --features gui-shot # window, with a frame counter
TINYCLIENT_FRAMES=1 cargo run -p tinyclient --features gui-shot 2>&1 | grep 'tinyclient: frame'
|
…times a second An animation's wake called window.refresh(), which dirties the whole window and bypasses view caching, and a GIF of 20 ms frames asked for a draw every 20 ms. The wake now notifies the views that drew the due pictures, and a playing picture is drawn again no sooner than 33 ms later; the frame shown still follows the clock, so fast files keep their speed and skip frames between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the root rather than notify it A view kept its earlier due time after another view's wake had already drawn it, and while no view is cached every wake draws every view, so two pictures on different beats drew the window about 50 times a second with gaps of 3 ms. The timer now fires no sooner than 33 ms after it last fired for a view, whichever view asks; nothing due is dropped, only held to the next slot. Notifying a view runs its observers, which refreshing never did. The window's root (the shell, which draws hover-play avatars itself) is now drawn by refreshing the window, so its observers, the Settings window among them, no longer run on every frame. The hover handler does the same, deferred until the window is let go and its root is known. A timer whose window has closed forgets that window's wakes. The scheduling is a pure struct driven by an injected clock in the tests: two views on 20 ms and 45 ms beats draw 31 times in the first second, never closer than 33 ms (without the floor: 52 draws, gaps down to 3 ms), every ask is met within 33 ms, and the timer goes idle once nothing asks again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bdd14a0 to
3fbf858
Compare
Fixes #39
Cause
anim::frame_nowscheduled each playing picture's next frame throughanim::wake, and the wake's timer calledwindow.refresh(). That dirties the whole window and sets GPUI'srefreshingflag, which bypasses view caching. The only floor on frame time wasframes::shown_for, which only slows frames of 10 ms or less, so a GIF with 16–20 ms frames redrew the whole window at about the display rate. The hover handler for hover-to-play pictures also calledwindow.refresh().Fix (
crates/tinyclient/src/gui/anim.rs)frame_nowrecords the view drawing the picture (window.current_view()). The window's single timer keeps a per-view due time (Due<EntityId>). When it fires it redraws only the views that are due, then sets itself for the rest. Views that are not due, and any view embedded with.cached, are not drawn again.FASTEST(33 ms) after it was drawn, and the timer fires no sooner thanFASTESTafter it last fired for a view, whichever view asks. The window-level floor matters because no view is.cachedyet, so every wake draws every view. Without it, a view kept its earlier due time even after another view's wake had already drawn it, and two pictures on different beats drew the window about 50 times a second with gaps as small as 3 ms. A view that is due is never dropped; it waits for the next slot, at most 33 ms. The frame shown is still worked out from elapsed time (frame_at), so a 20 ms GIF keeps its real speed and skips the frames in between.cx.notify(view)runs the view's observers, andwindow.refresh()never did.Shellis the window's root and paints hover-play avatars itself, so notifying it on every frame ranobserve_self → follow_openand the Settings window'scx.observe(&shell, …). That meant an open Settings window redrew at up to 30 fps while an avatar animated. When the due view is the window's root (found withupdate_rootin the timer), it is now drawn withwindow.refresh(). It is the same draw, but no observers run. The hover handler does the same. It is deferred withcx.deferuntil the window is let go, because only then can the root be told apart.Not in this PR:
.cachedviewsNo GUI view uses
.cached(...)yet. Until one does, GPUI still re-renders every uncached view when any view is dirty, so for now the CPU saving comes from the 30 fps window cap: about 1.6x fewer full draws for one 20 ms GIF, about 2x for 16 ms, and more with several pictures on different beats. The targeted notify is the groundwork that will let caching pay off later. Refreshing the root would also redraw cached children, but only when a picture the root paints itself is due.I did not add
.cachedbecause it isn't safe yet. Several child views get state pushed into them duringShell::renderwithout a notify. For example,MemberList::seeassignsself.scenewithout notifying, so a cached view would show stale content. CachingMemberList,ComposerandPaneneeds those paths checked first, so it belongs in a follow-up.Left as is: notifying a view that was released after it asked for a frame leaves an empty invalidator entry in GPUI. GPUI has no public way to check whether an
EntityIdis still alive, and this only happens once per released view.Validation
cargo test -p tinyclient --features gui: 351 passed, 0 failed, 1 ignored.cargo test -p tinyclient --features gui anim: 9 passed, 6 of them inanim.cargo test -p tinyclient(no gui): 12 passed, 0 failed.cargo clippy -p tinyclient --features gui --all-targets: no warnings inanim.rs. The warnings in other files are already onmain.Due), and the tests drive it with an injected clock:the_window_wakes_at_most_thirty_times_a_second_however_many_views_playsimulates a window where every draw makes every view ask again. With two views on 20 ms and 45 ms beats, it draws 31 times in the first second, never closer than 33 ms. Every ask is met within 33 ms, and the timer goes idle once nothing asks. A 100 ms picture alone is still drawn every 100 ms. With the window floor removed, the same test fails with 52 draws and gaps of 3–33 ms, which matches the review's finding.a_wake_draws_only_the_views_that_are_duechecks that only due views are drawn, that the sooner ask wins, that the timer only moves sooner, that a firing with nothing due keeps the timer, that the floor holds the next fire, and that the timer goes idle and re-arms.a_fast_animation_asks_no_sooner_than_fastest_and_keeps_its_timechecks the per-ask floor, and that a 20 ms GIF skips frames by the clock.Not verified: I did not run the app with animations playing, and did not profile CPU or frame rate live. The draw-rate figures come from the simulation above, not a measurement.
TINYCLIENT_FRAMES=1with thegui-shotfeature can confirm it on a live session.🤖 Generated with Claude Code